/************************************************************************
Copyright 2003, 2004, 2005, 2006, 2007, 2008 Grid Technology Research Center,
National Institute of Advanced Industrial Science and Technology.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
************************************************************************/

package jp.aist.gtrc.plus.scheduler.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

public class TimeUtil {

	private static final String ISO8601_FORMAT_PATTERN = "yyyy-MM-dd'T'HH:mm:ssZ";
	private static SimpleDateFormat iso8601Format;
	private static String localTimeZone;

	private static String getLocalTimeZone() {

		if (localTimeZone != null)
			return localTimeZone;

		// localTimeZone will be "+0900" etc.
		localTimeZone = String.format("%tz", new Date());
		return localTimeZone;

	}

	public static Calendar makeCalendarFromISO8601(String arg) {

		if ((arg == null) || (arg.length() == 0))
			return null;

		/*
		 * make Calendar object from ISO8601 format date&time string.
		 * See http://www.w3.org/TR/NOTE-datetime
		 *
		 * This function support:
		 * 		"yyyy-MM-DDTHH:mm:ss+zz:zz" or "...-zz:zz"
		 * 		"yyyy-MM-DDTHH:mm:ss+zzzz"  or "...-zzzz"
		 * 		"yyyy-MM-DDTHH:mm:ssZ"	(interpret as UTC)
		 * 		"yyyy-MM-DDTHH:mm:ss"	(interpret as local time)
		 * 1st ant 3rd are strict ISO8601 format,
		 * others are our original notation.
		 *
		 * ex:  "1994-11-05T08:15:30-05:00"
		 * digit 0123456789012345678901234
		 * Note: Java timezone format is "{+-}zzzz", without ":".
		 */

		String date;
		int i = arg.lastIndexOf(':');
		if (i == 22) {
			// arg may be "yyyy-MM-DDTHH:mm:ss+zz:zz" [strict ISO8601]
			if (arg.length() != 25)
				return null;
			// cut ':' in timezone for Java interpretation
			date = arg.substring(0, 22) + arg.substring(23);
			// date is "yyyy-MM-ddTHH:mm:ss+zzzz"
		} else if (i == 16) {
			if (arg.charAt(arg.length()-1) == 'Z') {
				// arg may be "yyyy-MM-DDTHH:mm:ssZ" [strict ISO8601 also]
				// time is expressed in UTC
				date = arg.substring(0, arg.length()-1) + "+0000";
				// date is "yyyy-MM-DDTHH:mm:ss+0000"
			} else if (arg.length() == 19) {
				date = arg + getLocalTimeZone();
				// date is "yyyy-MM-DDTHH:mm:ss+zzzz" or "..-zzzz"
			} else {
				// arg may be "yyyy-MM-DDTHH:mm:ss+zzzz"
				date = arg;
			}
		} else {
			return null;
		}

		try {
			Calendar c = Calendar.getInstance();
			if (iso8601Format == null)
				iso8601Format = new SimpleDateFormat(ISO8601_FORMAT_PATTERN);
			c.setTime(iso8601Format.parse(date));
			return c;
		} catch (ParseException e) {
			return null;
		}

	}

	private static boolean outOfRange(int value, int min, int max) {

		return ((value < min) || (max < value));

	}

	public static Calendar makeCalendarFromHHmm(String arg) {

		if ((arg == null) || (arg.length() == 0))
			return null;

		/*
		 * [1] TIME format:
		 * 	[a]	hh:mm		(hh as 24-hour format always)
		 * 	[b]	hh:mm:ss
		 * [2] DATE format
		 * 	[a]	DD
		 * 	[b]	DD/MM
		 *	[c]	DD/MM/YY	(year as 20YY, ex: 05 means 2005)
		 *	[d]	DD/MM/YYYY
		 *
		 * arg format: TIME or TIME_DATE
		 * 	[1a]	hh:mm
		 * 	[1b]	hh:mm:ss
		 * 	[1a+2a]	hh:mm_DD
		 * 	[1a+2b]	hh:mm_DD/MM
		 * 	[1a+2c]	hh:mm_DD/MM/YY
		 * 	[1a+2d]	hh:mm_DD/MM/YYYY
		 * 	[1b+2a]	hh:mm:ss_DD
		 * 	[1b+2b]	hh:mm:ss_DD/MM
		 * 	[1b+2c]	hh:mm:ss_DD/MM/YY
		 * 	[1b+2d]	hh:mm:ss_DD/MM/YYYY
		 */

		// set c as now
		Calendar c = Calendar.getInstance();

		/*
		 * Split time & date
		 */
		String[] x = arg.split("_");
		if ((x.length == 0) || (x.length > 2))
			return null;
		String time, date;
		time = x[0];
		date = (x.length == 2) ? x[1] : null;

		try {
			/*
			 * Split time to hh:mm or hh:mm:ss
			 */
			String[] y = time.split(":");
			int sec = 0;
			switch(y.length) {
			case 3:
				sec = Integer.parseInt(y[2]);
				if (outOfRange(sec, 0, 59))
					return null;
				// fall-through
			case 2:
				c.set(Calendar.MILLISECOND, 0);
				c.set(Calendar.SECOND, sec);
				int min = Integer.parseInt(y[1]);
				if (outOfRange(min, 0, 59))
					return null;
				c.set(Calendar.MINUTE, min);
				int hour = Integer.parseInt(y[0]);
				if (outOfRange(hour, 0, 23))
					return null;
				c.set(Calendar.HOUR_OF_DAY, hour);
				break;
			default:
				return null;
			}
			c.set(Calendar.MILLISECOND, 0);

			/*
			 * Split date to DD/MM/YYYY
			 */
			if (date != null) {
				String[] z = date.split("/");
				switch(z.length) {
				case 3:
					int year = Integer.parseInt(z[2]);
					if (year < 0)
						return null;
					if (year < 100)
						year += 2000;
					c.set(Calendar.YEAR, year);
					// fall-through
				case 2:
					int month = Integer.parseInt(z[1]);
					if (outOfRange(month, 1, 12))
						return null;
					// MONTH value must be 0...11
					c.set(Calendar.MONTH, month -1);
					// fall-through
				case 1:
					int day = Integer.parseInt(z[0]);
					if (outOfRange(day, 1, 31))
						return null;
					c.set(Calendar.DAY_OF_MONTH, day);
					break;
				default:
					return null;
				}
			}
		} catch (final NumberFormatException e) {
			// parseInt() failed
			return null;
		}

		return c;

	}

	public static Calendar makeCalendarFromPOSIX(String arg) {

		if ((arg == null) || (arg.length() == 0))
			return null;

		/*
		 * [[[[CC]YY]MM]DD]hhmm[.SS]
		 *
		 * ex: "Thu Jun  1 16:27:50 JST 2006" is equal to
		 * 		"200606011627.50"		(length = 15)
		 * 		  "0606011627.50"		(length = 13)
		 * 		    "06011627.50"		(length = 11)
		 * 		      "011627.50"		(length = 9)
		 * 		        "1627.50"		(length = 7)
		 *
		 * ex: "Thu Jun  1 16:27:00 JST 2006" is equal to
		 * 		"200606011627.00"  "200606011627"
		 * 		  "0606011627.00"    "0606011627"
		 * 		    "06011627.00"      "06011627"
		 * 		      "011627.00"        "011627"
		 * 		        "1627.00"          "1627"
		 */

		int length = arg.length();
		if ((length < 4) || (15 < length))
			return null;

		if (arg.charAt(length - 3) != '.') {
			arg = arg + ".00";
			length = arg.length();
		}

		Calendar c = Calendar.getInstance();

		try {
			switch (length) {
			case 15:	// "CCYYMMDDhhmm.SS"
				int year = Integer.parseInt(arg.substring(0, 4));
				if (year < 1000)
					return null;
				c.set(Calendar.YEAR, year);
				break;
			case 13:  // "YYMMDDhhmm.SS"
				int year2 = Integer.parseInt(arg.substring(0, 2));
				if (outOfRange(year2, 0, 99))
					return null;
				c.set(Calendar.YEAR, year2);
				break;
			case 11:	// "MMDDhhmm.SS"
				int nowMM = c.get(Calendar.MONTH) + 1;	// [0..11]
				int argMM = Integer.parseInt(arg.substring(0, 2)); // [1..12]
				if (argMM < nowMM) {
					// set 'c' as next year
					c.add(Calendar.YEAR, 1);
				}
				break;
			case 9:	// "DDhhmm.SS"
				int nowDD = c.get(Calendar.DAY_OF_MONTH);
				int argDD = Integer.parseInt(arg.substring(0, 2));
				if (argDD < nowDD) {
					// set 'c' as next month
					c.add(Calendar.MONTH, 1);
				}
				break;
			case 7:	// "hhmm.SS"
				int nowhhmm = 100 * c.get(Calendar.HOUR_OF_DAY) + c.get(Calendar.MINUTE);
				int arghhmm = Integer.parseInt(arg.substring(0, 4));
				if (arghhmm < nowhhmm) {
					// set 'c' as tomorrow
					c.add(Calendar.DAY_OF_MONTH, 1);
				}
				break;
			default:
				break;
			}

			int index = 0;
			switch (length) {
			case 15:	// "CCYYMMDDhhmm.SS"
				index += 2;
				// fall through
			case 13:  // "YYMMDDhhmm.SS"
				index += 2;
				// fall through
			case 11:	// "MMDDhhmm.SS"
				int month = Integer.parseInt(arg.substring(index, index + 2));
				if (outOfRange(month, 1, 12))
					return null;
				c.set(Calendar.MONTH, month -1);
				index += 2;
				// fall through
			case 9:	// "DDhhmm.SS"
				int day = Integer.parseInt(arg.substring(index, index + 2));
				if (outOfRange(day, 1, 31))
					return null;
				c.set(Calendar.DAY_OF_MONTH, day);
				index += 2;
				// fall through
			case 7:	// "hhmm.SS"
				int hour = Integer.parseInt(arg.substring(index, index + 2));
				if (outOfRange(hour, 0, 23))
					return null;
				c.set(Calendar.HOUR_OF_DAY, hour);
				index += 2;
				int min = Integer.parseInt(arg.substring(index, index + 2));
				if (outOfRange(min, 0, 59))
					return null;
				c.set(Calendar.MINUTE, min);
				index += 3;	// skip "mm."
				int sec = Integer.parseInt(arg.substring(index, index + 2));
				if (outOfRange(sec, 0, 59))
					return null;
				c.set(Calendar.SECOND, sec);
				c.set(Calendar.MILLISECOND, 0);
				break;
			default:
				return null;
			}

			return c;
		} catch (NumberFormatException e) {
			return null;
		}

	}

	public static void truncateDate(Calendar c) {

		c.set(Calendar.HOUR_OF_DAY, 0);
		c.set(Calendar.MINUTE, 0);
		c.set(Calendar.SECOND, 0);
		c.set(Calendar.MILLISECOND, 0);

	}

	public static Calendar makeCalendarFromDate(String arg) {

		if ((arg == null) || (arg.length() == 0))
			return null;

		/*
		 * arg must be "[yyyy-]MM-DD"
		 */
		Calendar c = Calendar.getInstance();
		String[] params = arg.split("-");
		int index = 0;
		try {
			switch (params.length) {
			case 3:
				// check year
				if (params[index].length() != 4) {
					return null;
				}
				int year = Integer.parseInt(params[index]);
				if (year < 0)
					return null;
				c.set(Calendar.YEAR, year);
				index++;
				// fall-through
			case 2:
				// check month
				if (params[index].length() != 2) {
					return null;
				}
				int month = Integer.parseInt(params[index]);
				if (outOfRange(month, 1, 12))
					return null;
				// MONTH value must be 0...11
				c.set(Calendar.MONTH, month -1);
				index++;
				// check day
				if (params[index].length() != 2) {
					return null;
				}
				int day = Integer.parseInt(params[index]);
				if (outOfRange(day, 1, 31))
					return null;
				c.set(Calendar.DAY_OF_MONTH, day);
				break;
			default:
				return null;
			}
		} catch (NumberFormatException e) {
			return null;
		}

		truncateDate(c);

		return c;
	}

	public static Calendar makeCalendar(String arg) {

		Calendar c;
		/*
		c = makeCalendarFromHHmm(arg);
		if (c != null)
			return c;
		*/

		c = makeCalendarFromISO8601(arg);
		if (c != null)
			return c;

		c = makeCalendarFromPOSIX(arg);
		if (c != null)
			return c;

		return null;

	}

	public static int hhmmssToSec(String time) {

		/*
		 * Support "hh:mm:ss" or "mm:ss" or "ss"
		 * We allow "hhhh:mm:ss", "mmmm:ss", "ssss" etc.
		 */
		String[] s = time.split(":");
		try {
			int sec, min = 0, hour = 0, index = 0;
			switch (s.length) {
			case 3:
				hour = Integer.parseInt(s[index]);
				if (hour < 0) {
					throw new NumberFormatException("hour must be positive");
				}
				index++;
				// fall-through
			case 2:
				min = Integer.parseInt(s[index]);
				if (min < 0) {
					throw new NumberFormatException("minute must be positive");
				}
				if ((s.length > 2) && (min >= 60)) {
					throw new NumberFormatException("minute must be 00..59");
				}
				index++;
				// fall-through
			case 1:
				sec = Integer.parseInt(s[index]);
				if (sec < 0) {
					throw new NumberFormatException("second must be positive");
				}
				if ((s.length > 1) && (sec >= 60)) {
					throw new NumberFormatException("second must be 00..59");
				}
				break;
			default:
				throw new NumberFormatException("invalid description: " + time);
			}
			return 3600 * hour + 60 * min + sec;
		} catch (NumberFormatException e) {
			throw new NumberFormatException("invalid description: " + time);
		}

	}

	public static final long secToMilli(int sec) {

		return 1000L * sec;

	}

	public static final int calToSec(Calendar c) {

		return (int)(c.getTimeInMillis() / 1000);

	}

	public static final Calendar makeCalendarFromSec(int sec) {

		return makeCalendarFromMilli(secToMilli(sec));

	}

	public static final Calendar makeCalendarFromMilli(long msec) {

		Calendar c = Calendar.getInstance();
		c.setTimeInMillis(msec);
		return c;

	}

	public static final double milliToHour(long msec) {

		return (msec / 3600000.0);

	}

	public static final long diffOfCal(Calendar start, Calendar end) {

		return (end.getTimeInMillis() - start.getTimeInMillis());

	}

	public static final boolean inPeriod(Calendar start, Calendar end) {

		return inPeriod(Calendar.getInstance(), start, end);

	}

	public static final boolean inPeriod(Calendar time, Calendar start, Calendar end) {

		long t = time.getTimeInMillis();
		long s = start.getTimeInMillis();
		long e = end.getTimeInMillis();
		return ((s <= t) && (t < e));

	}

}
